/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.spring.initializer;

import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

import javax.imageio.spi.ServiceRegistry;

import org.springframework.aop.aspectj.AspectJAdviceParameterNameDiscoverer.AmbiguousBindingException;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.genia.toolbox.basics.bean.Transformer;
import com.genia.toolbox.basics.exception.BundledException;
import com.genia.toolbox.basics.exception.technical.TechnicalException;
import com.genia.toolbox.basics.exception.technical.XmlException;
import com.genia.toolbox.basics.manager.XmlManager;
import com.genia.toolbox.basics.process.ProxyHelper;
import com.genia.toolbox.spring.provider.plugin.SpringProviderPlugin;
import com.genia.toolbox.spring.provider.plugin.impl.BasicSpringProviderPlugin;

/**
 * Initialization of Spring config file using Services. This class contains only
 * static method. It is used to generate the Spring config file.
 */
public class ProcessInitializer
{
  /**
   * a {@link Transformer} that generates {@link TreeSet}s.
   */
  private static final class TreeSetGenerator
      implements Transformer<String, Set<Element>>
  {
    /**
     * the transformation method.
     * 
     * @param object
     *          the object to convert
     * @return the converted object
     */
    public Set<Element> transform(String object)
    {
      return new TreeSet<Element>(PRIORITY_COMPARATOR);
    }
  }

  /**
   * class that allows us to compare bean definition depending on the added
   * attribute priority.
   */
  private static class PriorityComparator
      implements Comparator<Element>
  {

    /**
     * the comparaison method.
     * 
     * @param element1
     *          the first element to compare
     * @param element2
     *          the second element to compare
     * @return a negative number if e1 has a greater priority than e2 and a
     *         positive number if e2 has a greater priority than e1. If the 2
     *         priority are equals, then a {@link AmbiguousBindingException} is
     *         thrown.
     * @throws AmbiguousBindingException
     *           if the 2 element have the same priority
     */
    public int compare(Element element1, Element element2)
        throws AmbiguousBindingException
    {
      int res = getPriority(element2) - getPriority(element1);
      if (res == 0) {
        throw new AmbiguousBindingException("2 beans with the same name (" + getBeanName(element1) + ") and the same priority (" + getPriority(element2) + ") have been defined.");
      }
      return res;
    }

  }

  /**
   * xml attribute of the name property.
   */
  private static final String NAME_ATTRIBUTE = "name";

  /**
   * xml attribute of the id property.
   */
  private static final String ID_ATTRIBUTE = "id";

  /**
   * xml tag for bean.
   */
  private static final String BEAN_TAG = "bean";

  /**
   * xml tag for alias.
   */
  private static final String ALIAS_TAG = "alias";

  /**
   * xml attribute of the alias property.
   */
  private static final String ALIAS_ATTRIBUTE = "alias";

  /**
   * xml tag for description.
   */
  private static final String DESCRIPTION_TAG = "description";

  /**
   * xml tag for import.
   */
  private static final String IMPORT_TAG = "import";

  /**
   * xml attribute of the priority property.
   */
  private static final String PRIORITY = "priority";

  /**
   * xml attribute of the depends-on property.
   */
  private static final String DEPENDS_ON = "depends-on";

  /**
   * xml attribute of the needs-by property.
   */
  private static final String NEEDS_BY = "needs-by";

  /**
   * location of the xslt file that allows to generate the final spring config
   * file from the aggregation of single spring config file.
   */
  private static final String SPRING_XML = "/com/genia/toolbox/spring/initializer/spring.xml";

  /**
   * the <code>XmlManager</code> that allows the manipulation of xml files.
   */
  private static XmlManager xmlManager = null;

  /**
   * the <code>GenericApplicationContext</code> generated from the final
   * spring config file from the aggregation of single spring config file.
   */
  private static transient GenericApplicationContext applicationContext = null;

  /**
   * the static {@link PriorityComparator} to use.
   */
  private static final PriorityComparator PRIORITY_COMPARATOR = new PriorityComparator();



  /**
   * add a dependency from the bean defined by beanElement to the bean of name
   * beanName.
   * 
   * @param beanElement
   *          the {@link Element} of the bea having a priority
   * @param beanName
   *          the name of the bean the element depends on
   */
  public static void addDependency(Element beanElement, String beanName)
  {
    String dependsOnAttribute = xmlManager.getAttribute(beanElement, DEPENDS_ON);
    if (dependsOnAttribute == null) {
      dependsOnAttribute = beanName;
    }
    else {
      dependsOnAttribute += ("," + beanName);
    }
    beanElement.setAttribute(DEPENDS_ON, dependsOnAttribute);
  }



  /**
   * This method free all static resources used by this class.
   */
  public static void freeResources()
  {
    xmlManager = null;
  }



  /**
   * release the current {@link ApplicationContext} so that the next call to
   * <code>getApplicationContext</code> will construct a new one.
   */
  public static void releaseApplicationContext()
  {
    synchronized (ProcessInitializer.class) {
      if (applicationContext != null) {
        applicationContext.close();
        System.gc();
        applicationContext = null;
        ApplicationContextHolder.setApplicationContext(null);
      }
    }
  }



  /**
   * getter for the applicationContext property.
   * 
   * @return the applicationContext
   * @throws BundledException
   *           when an unexpected error occurred
   */
  public static ApplicationContext getApplicationContext()
      throws BundledException
  {
    synchronized (ProcessInitializer.class) {
      if (applicationContext == null) {
        try {
          final File springConfigFile = File.createTempFile("spring-config", ".xml");
          springConfigFile.deleteOnExit();
          ProcessInitializer.init();
          ProcessInitializer.saveConfigFile(springConfigFile);
          applicationContext = (GenericApplicationContext) getApplicationContext(new FileSystemResource(springConfigFile));
          springConfigFile.delete();
          ApplicationContextHolder.setApplicationContext(applicationContext);
        }
        catch (final Exception e) {
          throw new TechnicalException(e);
        }
        finally {
          ProcessInitializer.freeResources();
        }
      }
    }
    return applicationContext;
  }



  /**
   * generate an <code>ApplicationContext</code> from a config file.
   * 
   * @param resource
   *          the location of the config file
   * @return the <code>ApplicationContext</code>
   */
  public static ApplicationContext getApplicationContext(final Resource resource)
  {
    final GenericApplicationContext applicationContext = new GenericApplicationContext();
    final XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(applicationContext);
    xmlReader.loadBeanDefinitions(resource);
    applicationContext.refresh();
    return applicationContext;
  }



  /**
   * retrieve the final Spring config file.
   * 
   * @return the final Spring config file
   * @throws XmlException
   *           if there is a problem with the XML manipulation
   * @throws TechnicalException
   *           if an error occured
   */
  public static Document getConfigFile()
      throws XmlException, TechnicalException
  {
    int generatedIndex = 0;
    final Map<String, Set<Element>> beanElements = ProxyHelper.getSafeMap(new ConcurrentHashMap<String, Set<Element>>(), new TreeSetGenerator());
    final Document res = xmlManager.parse(ProcessInitializer.class.getResourceAsStream(SPRING_XML));
    final Element rootNode = res.getDocumentElement();
    final Element descriptionNode = (Element) rootNode.getElementsByTagName(DESCRIPTION_TAG).item(0);
    for (final InputStream inputStream : getInputStreams()) {
      try {
        final Element documentElement = xmlManager.parse(inputStream).getDocumentElement();
        ELEMENT: for (Element element : xmlManager.getElements(documentElement.getChildNodes())) {
          if ((element).getTagName().equals(DESCRIPTION_TAG)) {
            continue ELEMENT;
          }
          if ((element).getTagName().equals(IMPORT_TAG)) {
            rootNode.insertBefore(res.importNode(documentElement.removeChild(element), true), descriptionNode.getNextSibling());
            continue ELEMENT;
          }
          if ((element).getTagName().equals(BEAN_TAG) || (element).getTagName().equals(ALIAS_TAG)) {
            String beanName = getBeanName(element);
            if (beanName != null) {
              Element beanElement = (Element) documentElement.removeChild(element);
              beanElements.get(getBeanName(beanElement)).add(beanElement);
            }
            else if (xmlManager.getAttribute(element, NEEDS_BY) != null) {
              String generatedName = "generated_bean_id" + (generatedIndex++);
              element.setAttribute(ID_ATTRIBUTE, generatedName);
              Element beanElement = (Element) documentElement.removeChild(element);
              beanElements.get(getBeanName(beanElement)).add(beanElement);
            }
            else {
              String generatedName = "generated_bean_id" + (generatedIndex++);
              element.setAttribute(ID_ATTRIBUTE, generatedName);
            }
          }
        }
        for (Element element : xmlManager.getElements(documentElement.getChildNodes())) {
          rootNode.appendChild(res.importNode(element, true));
        }

      }
      catch (final DOMException e) {
        throw new XmlException(e);
      }
    }
    for (Set<Element> set : beanElements.values()) {
      Element beanElement = set.iterator().next();
      if (set.size() > 1) {
        set.clear();
        set.add(beanElement);
      }
      if (xmlManager.getAttribute(beanElement, PRIORITY) != null) {
        beanElement.removeAttribute(PRIORITY);
      }
      String needsBy = xmlManager.getAttribute(beanElement, NEEDS_BY);
      if (needsBy != null) {
        Set<Element> otherSet = beanElements.get(needsBy);
        if (otherSet != null) {
          addDependency(otherSet.iterator().next(), getBeanName(beanElement));
        }
        beanElement.removeAttribute(NEEDS_BY);
      }
    }
    for (Set<Element> set : beanElements.values()) {
      Element beanElement = set.iterator().next();
      rootNode.appendChild(res.importNode(beanElement, true));
    }
    xmlManager.stripWhiteSpaceNodes(res);
    return res;
  }



  /**
   * Initialization. It must be called before any other methods. We also set the
   * {@link Locale} to an english one to insure that if no locale is defined by
   * the application through
   * {@link org.springframework.context.i18n.LocaleContextHolder}, the locale
   * will be the default one.
   */
  public static void init()
  {
    Locale.setDefault(new Locale("en"));
    xmlManager = (XmlManager) new XmlBeanFactory(new UrlResource(ProcessInitializer.class.getResource(BasicSpringProviderPlugin.XML_MANAGER_CONFIG_FILE_RESOURCE))).getBean("xmlManager");
  }



  /**
   * Save the final Spring config file to the <code>File</code> given in
   * parameter.
   * 
   * @param file
   *          where to save the Spring config file.
   * @throws XmlException
   *           if there is a problem with the XML manipulation
   * @throws TechnicalException
   *           if an error occured
   */
  public static void saveConfigFile(final File file)
      throws XmlException, TechnicalException
  {
    xmlManager.print(getConfigFile(), file);
  }



  /**
   * returns the name of the bean described by an {@link Element}.
   * 
   * @param beanElement
   *          the {@link Element} describing a bean
   * @return the name of the bean described by an {@link Element}.
   */
  private static String getBeanName(Element beanElement)
  {
    if (beanElement.getTagName().equals(BEAN_TAG)) {
      String beanName = xmlManager.getAttribute(beanElement, ID_ATTRIBUTE);
      if (beanName != null) {
        return beanName;
      }
      beanName = xmlManager.getAttribute(beanElement, NAME_ATTRIBUTE);
      if (beanName != null) {
        return beanName;
      }
      return null;
    }
    else if (beanElement.getTagName().equals(ALIAS_TAG)) {
      return xmlManager.getAttribute(beanElement, ALIAS_ATTRIBUTE);
    }
    return null;
  }



  /**
   * returns the priority of the bean described by an {@link Element}.
   * 
   * @param beanElement
   *          the {@link Element} describing a bean
   * @return the priority of the bean described bu an {@link Element}. This is
   *         the value of the atribute priority or <code>0</code> if no such
   *         attribute exists.
   */
  private static int getPriority(Element beanElement)
  {
    String beanPriority = xmlManager.getAttribute(beanElement, PRIORITY);
    if (beanPriority != null) {
      return Integer.parseInt(beanPriority);
    }
    return 0;
  }



  /**
   * return the list of <code>URL</code> of springs config files.
   * 
   * @return the list of <code>URL</code> of springs config files
   * @throws TechnicalException
   *           when a configuration file does not exists
   */
  private static List<InputStream> getInputStreams()
      throws TechnicalException
  {
    final Iterator<SpringProviderPlugin> iterator = ServiceRegistry.lookupProviders(SpringProviderPlugin.class);
    final ArrayList<InputStream> result = new ArrayList<InputStream>();
    while (iterator.hasNext()) {
      result.addAll(iterator.next().getConfigurations());
    }
    return result;
  }



  /**
   * private constructor.
   */
  private ProcessInitializer()
  {
  }
}
